# encoding: utf-8
#
# The MIT License
#
# Copyright (c) 2010 Anton Moiseev
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

"""clone projects, groups and all repositories from a Kiln account

This extension provides convenient way to clone groups, projects and all
repositories from an Fog Creek Software Kiln account. It smoothly integrates
with your Mercurial instance and is really natural to use like
the standard :hg:`clone` command.

If you don't want to type FogBugz API token every time you use kilnclone,
add following entry in the [ui] section of your hgrc file::

    [ui]
    fogbugzapitoken = db400a2669748bb1a8f4f165739b40bb # replace by your token
    
Examples::

    1)  The following example shows how to clone a specific group of
        Mercurial repositories out of specific Kiln project:
        
        hg kclone -p TestProject -g archive account.kilnhg.com
               
    2)  The following example shows how to clone the entire Kiln project
        with all included groups of Mercurial repositories:
        
        hg kclone -p TestProject account.kilnhg.com
        
    3)  The following example shows how to clone all projects from an Kiln
        account to specific directory using explicitly specified
        FogBugz API token.
        
        hg kclone -t db400a..40bb account.kilnhg.com MyProjects
"""

import os
import sys
import urllib
import urllib2
import urlparse
from mercurial import commands, util

try:
    import json
except ImportError:
    try:
        import simplejson as json
    except ImportError:
        sys.exit('For versions of Python earlier than 2.6, you must ' +
            'install simplejson (http://pypi.python.org/pypi/simplejson/).')

def get_repos(ui, server, token):
    """
    Gets the list of all repositories from Kiln account, sorted by the Kiln's
    "full name" of each repository.
    """
    ui.status('Getting the list of repositories from %s ...\n' % server)
    
    # Loading information about repositories
    # from specified account in JSON format.
    url = 'https://%s/kiln/Api/Repos/?token=%s' % (server, token)
    data = json.load(urllib2.urlopen(url))
    
    # If there is an error, abort the entire process.
    if 'error' in data:
        raise util.Abort(data['error'])
    
    ui.status('Found %d repositories\n' % len(data))
    return sorted(data, lambda x, y: cmp(x['fullName'], y['fullName']))

def get_token(ui):
    """
    Try to get FogBugz API token from all available places.
    """
    token = ui.config('ui', 'fogbugzapitoken', default='')
    if not token:
        token = ui.prompt('Your FogBugz API token: ', default='')
    if not token:
        raise util.Abort('FogBugz API token is required.')
    return token

def encode_stdout(message):
    """
    Encodes the message into appropriate sys.stdout
    encoding of the current environment.
    """
    if os.name == 'nt':
        message = message.replace(u'»', '>>')
    return message.encode(sys.stdout.encoding, 'xmlcharrefreplace')

def encode_stderr(message):
    """
    Encodes the message into appropriate sys.stderr
    encoding of the current environment.
    """
    return message.encode(sys.stderr.encoding, 'xmlcharrefreplace')

def encode_url(url):
    """
    URLs returned by Kiln may have unencoded Unicode characters in
    them. Encode them.
    """
    url_parts = list(urlparse.urlsplit(url.encode('utf-8')))
    url_parts[2] = urllib.quote(url_parts[2])
    return urlparse.urlunsplit(url_parts)

def clone_repo(ui, clone_url, target_dir, fullName):
    """
    Clones the specified repository. Returns True if successful. If
    the clone fails, prints an error message and returns False,
    without aborting the entire process.
    """
    # If the filesystem does not use Unicode (from Python’s perspective),
    # convert target_dir to plain ASCII.
    if not sys.getfilesystemencoding().upper().startswith('UTF'):
        target_dir = target_dir.encode('ascii', 'xmlcharrefreplace')
    # Mercurial API throws an exception if target_dir is passed (below)
    # as unicode object, so it's required to be converted to string.
    else:
        target_dir = str(target_dir)
    
    # If directory already exists...
    if os.path.isdir(target_dir):
        # ... and is not empty...
        if os.listdir(target_dir):
            # ... just skip it, without aborting the entire process.
            ui.warn(encode_stderr(('Warning: directory %s already exists ' +
                'and is not empty, it will be skipped.\n') % target_dir))
            return False
    # Otherwise create required catalogs tree.
    else:
        os.makedirs(target_dir)
    
    ui.status(encode_stdout('\nCloning "%s" repository\n' % fullName))
    commands.clone(ui, clone_url, dest=target_dir)
    return True

def go_through_repos(ui, repos, dest):
    """
    Go through specified repositories, construct directories for cloning
    and try to clone them.
    """
    # Keep track of overall success status. We continue cloning
    # even if there’s an error.
    overall_success = True
    
    for repo in repos:
        # The "full name" from Kiln is the project name, plus any
        # group name and the repo name. Components are separated by
        # a right angle quote (»). Turn this into a path by
        # converting angle quotes into path separators.
        parts = repo['fullName'].split(u'»')
        parts = [_.strip() for _ in parts]
        subdir = os.path.join(*parts)
        
        clone_url = encode_url(repo['cloneUrl'].strip('"'))
        target_dir = unicode(os.path.join(dest, subdir))
        result = clone_repo(ui, clone_url, target_dir, repo['fullName'])
        overall_success = overall_success and result
    
    return overall_success

def kilnclone(ui, server, dest='', **opts):
    """clone group, project or all repositories
    
    Clone group, project with specified names or all the projects
    from specified Kiln account.
    """
    # Since default Mercurial installer for Windows uses internal instance
    # of Python, there is no any path to Python's libraries by default
    # (in particular this extension requires "json" module). And sys.prefix
    # points to Mercurial installation directory, so I can't add path to
    # necessary Python's library automatically. Thus, Windows users must
    # manually add path to Python's library directory into PYTHONPATH
    # environment variable (e.g.: "C:\Python26\lib").
    if os.name == 'nt':
        pythonpath_var = os.path.expandvars('%PYTHONPATH%')
        [sys.path.append(_) for _ in pythonpath_var.split(';')]

    # Retrieve command parameters.
    token = opts.get('token')
    group = opts.get('group')
    project = opts.get('project')
    
    # Check FogBugz API token.
    if not token:
        token = get_token(ui)
    
    # Get all repositories from specified Kiln account.
    repos = get_repos(ui, server, token)
    
    if len(repos) == 0:
        util.Abort('There is no repository in specified Kiln account. ' +
            'Nothing to clone.')
        return
    
    process_success = True
    
    # If project and group parameters are not specified,
    # clone all projects and all groups from specified account.
    if not project and not group:
        # If destination directory is not set,
        # use account name for that purpose.
        if not dest:
            dest = server.split('.')[0].strip()
        
        process_success = go_through_repos(ui, repos, dest)
    
    # If group parameter is not specified,
    # clone all groups from specified project.
    elif not group:
        # Construct filter to pick out specified project.
        project_filter = ('/Kiln/Repo/%s' % project).replace(' ', '-')
        
        # Filter Mercurial repositories within specified project.
        repos = [_ for _ in repos if _['url'].lower().startswith(
            project_filter.lower())]
        
        # If there is no project with specified name, abort the entire process.
        if len(repos) == 0:
            raise util.Abort(('There is no "%s" project in specified Kiln ' +
                'account. Nothing to clone.') % project)
            return
        
        process_success = go_through_repos(ui, repos, dest)
    
    # If project parameter is not specified, try to find out a project
    # contains specified group and clone that group.
    elif not project:
        # Filter repositories using specified group.
        repos = [_ for _ in repos if _['url'].split('/')[4].lower() ==
            group.lower()]
        
        # If there is more than one group with specified name,
        # we are not sure what to clone, so we just abort the process.
        if len(repos) > 1:
            raise util.Abort(('There is more than one "%s" group in '
                'specified Kiln account across all projects. We are not sure' +
                ' what to clone.') % group)
            return
        # If there is no group with specified name within any project,
        # abort the entire process.
        elif len(repos) == 0:
            raise util.Abort(('There is no "%s" group in specified Kiln ' +
                'account within any project. Nothing to clone.') % group)
            return
        process_success = go_through_repos(ui, repos, dest)
    
    # If project and group parameters are specified,
    # clone specified group from specified project.
    else:
        # Construct filter to pick out specified project and group.
        project_filter = ('/Kiln/Repo/%s/%s' % (project, group)).replace(
            ' ', '-')
        
        # Filter repositories within specified group and project.
        repos = [_ for _ in repos if _['url'].lower().startswith(
            project_filter.lower())]
        
        # If there is no group with specified name within specified project,
        # abort the entire process.
        if len(repos) == 0:
            raise util.Abort(('There is no "%s" group within "%s" project ' +
                'in specified Kiln account. Nothing to clone.')
                % (group, project))
            return
        
        process_success = go_through_repos(ui, repos, dest)
    
    # Notify user if an error has occured during the cloning process.
    if process_success:
        ui.note('The cloning process is completed succesfully.\n')
    else:
        ui.warn('\nThe cloning process is completed with errors. See output' +
            ' messages above.\n')

commands.norepo += " kclone"
cmdtable = {
    'kclone' : (kilnclone,
        [('t', 'token', '', 'FogBugz API token'),
         ('g', 'group', '', 'a specific group you would like to clone'),
         ('p', 'project', '', 'a specific project you would like to clone')],
         'hg kclone [OPTION]... SOURCE [DEST]')
}
